FastAPI でRESTful Webサービスの実装 Part1
https://gyazo.com/59ad44865c354fea05eac3cf479a23a1
FastAPIについて
FastAPI は非同期処理に対応したRESTful Webアプリケーション開発のマイクロフレームワークです。他にも次のような特徴があります。
高速:
Uvicornは、uvloopとhttptoolsで構築された非同期I/Oに対応した超高速ASGIサーバーで、WSGIで処理できないHTTP/2およびWebSocketsのサポートも提供します。
starletteによるJSONやJinja2テンプレートエンジンをサポート。
コーディングの高速化
機能の開発速度を約200%から300%向上させます。
バグの減少:
開発者の人為的なエラーの約40%を削減します。
直感的:
優れたエディターサポート。エディターで補完しやすいように十分なアノテーションがされています。このため、結果的にデバッグや開発時間が短縮となります。
簡単:
使いやすく学習しやすいように設計されています。秀逸なドキュメントも用意されています。
そのすばらしいドキュメントを読む時間も短かくてすみます。
APIドキュメントを自動生成してくれるためフロントエンド開発者との連携が楽になります。
簡潔:
各パラメータ宣言からの複数の機能を提供されるため、コードの重複を最小限に抑えることができます。結果的にバグが少なくなります。
堅牢:
本番運用可能なコード。
標準準拠:
OpenAPIに準拠。OpenAPIは、以前はSwaggerとして知られていました。およびJSONスキーマに基づき完全互換性が提供されています。
コマンドライン
Typer によるコマンドライン・インタフェースの提供 FlaskとClick のように、わずかなコードでコマンドラインスクリプトを作成することができます。
インストール
既にCONDA環境になっているのであれば抜けておきましょう。
code: bash
$ conda deactivate
conda環境を作ります。このとき python と curl をインストールしておきます。
code: bash
$ conda create -y -n fastapi_todo python=3.6 curl
$ conda activate fastapi_todo
FastAPIと関連パッケージをインストールしておきましょう。
code: bash
$ pip install fastapi uvicorn
FastAPIの動作確認
アプリケーションのディレクトリを作成します。
code: bash
$ mkdir -p $HOME/fastapi/todo_apiv1
$ cd $HOME/fastapi/todo_apiv1
動作確認のために次のようなapp.py を作成しましょう。
code: python
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"Hello": "World"}
この段階でFastAPIが正常に動作するか確認しましょう。
FastAPI はFlaskやDjangoのように開発用サーバが組み込まれていないので、uvicorn を使ってアプリケーションを起動します。
code: bash
$ uvicorn --reload --port 8080 app:app
あるいは、uvicorn.run() を使って起動します。
code: Python
from fastapi import FastAPI
import uvicorn
app = FastAPI()
# ...
if __name__ == '__main__':
uvicorn.run(app=app, port=8080)
ブラウザでhttp://127.0.0.1:8080/ にアクセスして{"Hello":"World"}が表示されれば、FastAPI は正常に動作していることになります。
FastAPIはブラウザから参照できるため開発がとても楽になります。
また、APIドキュメントは定義から自動生成されます。
関数に記述した docstrings も表示してくれます。
ブラウザでhttp://127.0.0.1:8080/docs にアクセスしてみましょう。
https://gyazo.com/0819647d09aae28e0fe76ac9c3b9f6cf
FastAPI は ReDoc をサポートしているため、 ブラウザでhttp://127.0.0.1:8080/redoc にアクセスしてもAPIドキュメントを参照することができます。
https://gyazo.com/70c610be52fe362c2cf5d78b90e38a84
HTTPメソッドのマッピング
WebサービスTODOの仕様を少し変更して、PUTメソッドではなくPATCHメソッドを使うようにしています。
table: APIとHTTPメソッド
HTTPメソッド URI アクション
タスクリソースは次の情報を持つものとします。
id:タスクを示す一意の識別子。Integer型。
title:タスクのタイトル。タスクについての短い説明。 String型。
description:タスクの詳細。タスクについての詳細な説明。 Text型。
done:タスクの完了状態。 Boolean型。
タスクリソースの定義
タスクリソースについては、今回はREST APIに集中するために単純に辞書型のリストとして定義します。
code: tasks.py
tasks = [
{
'id': 1,
'title': 'Buy Beer',
'description': 'IPA 6 bottles',
'done': False
},
{
'id': 2,
'title': 'Buy groceries',
'description': 'Beef, Tofu, Sting Onion',
'done': False
}
]
FastAPIでのHTTPプロトコルとの対応
FastAPI ではHTTPプロトコルのメソッドに対応するデコレータは次のものとなります。
table: FastAPIのメソッド対応
デコレータ HTTPプロトコルメソッド
@app.get() GETメソッド
@app.post() POSTメソッド
@app.put() PUTメソッド
@app.patch() PATCHメソッド
@app.delete() DELETEメソッド
GETメソッドでのタスクの取得
次のコードはGETメソッドでタスク一覧を取得するためのものです。
code: python
from fastapi import FastAPI, HTTPException
from tasks import tasks
app = FastAPI()
@app.get("/todo/api/v1.0/tasks")
def get_tasklist():
return {"data": tasks}
同じGETメソッドでパラメタとしてタスクIDをを受け取るときは、次のようにします。
code: Python
@app.get("/todo/api/v1.0/tasks/{id}")
def get_task(id):
task_id = int(id)
task = [task for task in tasks if task'id' == task_id] if len(task) == 0:
raise HTTPException(status_code=404, detail="Not found")
else:
ここで、get_task() はURLからパラメタを引数id として文字列で受け取ります。
このとき、FastAPI では次のようにタイプアノテーションを記述して、整数で受け取ることもできます。
code: Python
@app.get("/todo/api/v1.0/tasks/{id}")
def get_task(id: int):
task = [task for task in tasks if task'id' == id] if len(task) == 0:
raise HTTPException(status_code=404, detail="Not found")
else:
POSTメソッドでタスクを登録
code: python
@app.post("/todo/api/v1.0/tasks")
def create_task(request_data: dict):
task = {
'done': False
}
tasks.append(task)
return {"data": request_data}
ここで、@app.post() でデコレートした関数の引数は、リクエストされたPOSTメソッドのボディーが渡されます。
PATCHメソッドでタスクを修正
code: Python
@app.patch("/todo/api/v1.0/tasks/{id}")
def patch_task(id: int, request_data: dict):
task = [task for task in tasks if task'id' == id] if len(task) == 0:
raise HTTPException(status_code=404, detail="Not found")
for key in request_data.keys():
DELETEメソッドでタスクの削除
code: python
@app.delete("/todo/api/v1.0/tasks/{id}")
def delete_task(id: int):
task = [task for task in tasks if task'id' == id] if len(task) == 0:
raise HTTPException(status_code=404, detail="Not found")
return {"result": "OK"}
ここまでのソースコードの整理
code: Python
from fastapi import FastAPI, HTTPException
from tasks import tasks
app = FastAPI()
@app.get("/todo/api/v1.0/tasks/{id}")
def get_task(id: int):
task = [task for task in tasks if task'id' == id] if len(task) == 0:
raise HTTPException(status_code=404, detail="Not found")
else:
@app.get("/todo/api/v1.0/tasks")
def get_tasklist():
return {"data": tasks}
@app.post("/todo/api/v1.0/tasks")
def create_task(request_data: dict):
task = {
'done': False
}
tasks.append(task)
return {"data": request_data}
@app.patch("/todo/api/v1.0/tasks/{id}")
def patch_task(id: int, request_data: dict):
task = [task for task in tasks if task'id' == id] if len(task) == 0:
raise HTTPException(status_code=404, detail="Not found")
for key in request_data.keys():
@app.delete("/todo/api/v1.0/tasks/{id}")
def delete_task(id: int):
task = [task for task in tasks if task'id' == id] if len(task) == 0:
raise HTTPException(status_code=404, detail="Not found")
return {"result": "OK"}
FastAPIでは、Python の関数アノテーションで型を指定を使い、簡潔で直感的に記述するだけで次の機能が提供されます。
エディターのサポート:エラーチェック、オートコンプリートなど
データの解析(Parse)
データ検証(Validation)
APIアノテーションと自動ドキュメント
他のフレームワークと比較しても、FastAPIは秀逸なものとなります。
レスポンスデータ
FastAPI は RESTFul APIに特化しているわけではありません。HTMLやJinja2テンプレートエンジンでレンダリングしたデータを返すことも、リクエストデータとしてファイルをアップロードを受けとることもでき、通常のWebアプリケーションを開発するフレームワークともなります。
非同期処理
例としてとりあげているWebサービスTODOではほとんど時間がかかるような処理がありませんが、実際のWebサービスでは時間がかかる処理もあります。このような時間がかかる処理をバックグランドで処理させて、その終了を待たずに応答を先に返したいことがあります。
バックグラウンド処理の実装はなかなか面倒なのですが、非同期処理に対応しているFastAPIでは驚くほど簡単になります。
code: python
from fastapi import BackgroundTasks
from time import sleep
from datetime import datetime
def slow_task(numbers: int):
sleep(numbers)
print(f'sleep done. {datetime.utcnow()}')
@app.get('/{numbers}')
async def backjobs(numbers: int, background_tasks: BackgroundTasks):
background_tasks.add_task(slow_task, numbers)
return {"result": f"finish {datetime.utcnow()}"}
BackgroundTasks型で受け取ったインスタンスオブジェクトの add_task()メソッドを使ってバックグラウンドで処理を登録することと、ビュー関数に async をつけておくだけです。
これで、slow_task()の処理結果を待つことなくクライアントに応答を返してくれます。
参考: